<?php


/**
 * Semiseekable stream handler class.
 * it makes possible to make the first 'BUFFER_SIZE' bytes of a nonseekable stream seekable
 * 
 * @package Sabre
 * @subpackage DAV
 * @license http://code.google.com/p/sabredav/wiki/License Modified BSD License
 */
class SemiSeekableStream {
	
	const BUFFER_SIZE=8192; //min 8192 bytes. the smallest possible buffersize
	
	protected $resource;
	protected $buffer=array();
	protected $varname;
	protected $position=0;
	protected $metadata;
	
	public function __construct() {
		
		//nulls in array are uninitialized areas
		$this->buffer=array_fill(0,self::BUFFER_SIZE,null);
	}
	
	/**
     * uses a stream handler from a global variable
     *
     * usage:
     * $GLOBALS['foo']=$nonseekable_stream;
     * $new_handler=fopen('semiseekable://foo','r+');
     * 
     *
     * @param string $path
     * @param string $mode
     * @param int $options
     * @param string &$opened_path
     * @return bool
     */
    public function stream_open($path , $mode , $options , &$opened_path) {
		
		$url = parse_url($path);
        $this->varname = $url["host"];
		$this->resource=$GLOBALS[$this->varname];
		
		$this->position=ftell($this->resource);
		$this->metadata=stream_get_meta_data($this->resource);
		
		return is_resource($this->resource);
	}

    /**
     * Closes the stream
     *
     */
    public function stream_close() {
		return fclose($this->resource);
	}
	
	
	/**
	 * Custom function to determine if the stream is seekable
	 *
	 */ 
	public function is_seekable() {
		if ((is_array($this->metadata)) && (array_key_exists('seekable',$this->metadata))) {
			return $this->metadata['seekable'];
		}
		
		return false;
	}
	
	/**
     * Reading from stream.
     * Depending on current position and count, fread and/or internal buffer will be used
     *
     * @param int $count
     * @return string
     */
    public function stream_read($count) {
		
		if ($this->is_seekable()) {
			return fread($this->resource,$count);
		}
		else {
			if ($this->position>self::BUFFER_SIZE) {
				//don't update the position, we don't need it anymore
				return fread($this->resource,$count);
			}
			else {
				//current position is in the bufferable area
				
				$real_pos=ftell($this->resource);
				
				if ($real_pos==$this->position) {
					if ($count>self::BUFFER_SIZE) {
						//reading without maintaining the buffer
						//because: after executing read, the file pointer is out of the bufferable range
						//so no further seek is possible
						$this->position+=$count;
						return fread($this->resource,$count);
					}
					else {
						//request is bufferable
						
						$start_pos=ftell($this->resource);
						$readed=fread($this->resource,$count);
						$new_position=ftell($this->resource);
						
						$p=0;
						for ($i=$this->position;$i<$new_position;$i++) {
							$this->buffer[$i]=$readed[$p];
							$p++;
						}						
						$this->position=$new_position;
						
						return $readed;
					}
				}
				
				//if here: stream was (least once) rewinded
				
				$readed='';
				$count_ok=0;
				$end_of_buff_at=0;
				for ($i=$this->position;(($i<self::BUFFER_SIZE) && ($count_ok<$count));$i++) {
					if (is_null($this->buffer[$i])) {
						$end_of_buff_at=$i;
						break;
					}
					$readed.=$this->buffer[$i];
					$count_ok++;
					$this->position++;
				}
				
				$count-=$count_ok;
				
				$readed_2='';
				if ($count>0) {
					//reading more as the bufferable length
					$readed_2=fread($this->resource,$count);
					
					$count_ok=0;
					for ($i=$this->position;($i<self::BUFFER_SIZE && $count_ok<$count);$i++) {
						if (isset($readed_2[$count_ok])) {
							$this->buffer[$i]=$readed_2[$count_ok];
							$count_ok++;
						}
						else {
							//end of result
							break;
						}
					}
					
					//at this point no seeking is possible anymore
					$this->position=ftell($this->resource);
					
				}
				
				return $readed.$readed_2;
			}
		}
	}
	
	/**
     * Returns the current stream position
     *
     * @return int
     */
    public function stream_tell() {
		if ($this->is_seekable()) {
			return ftell($this->resource);
		}
		else {
			if ($this->position<self::BUFFER_SIZE) {
				return $this->position;
			}
			else {
				return ftell($this->resource);
			}
		}
	}

    /**
     * sets the stream pointer if possible
     *
     * @param int $offset
     * @param int $whence = SEEK_SET
     * @return bool
     */
    function stream_seek($offset, $whence=SEEK_SET) {
		
		if ($this->is_seekable()) {
			//simple seek. source stream is seekable
			return fseek($this->resource,$offset,$whence);
		}
		else {
			//stream is not seekable, but seeking is possible in the buffered range
			
			switch ($whence) {
				case SEEK_SET:
					if (($offset>=0) && ($offset<self::BUFFER_SIZE) && ($this->position<=self::BUFFER_SIZE)) {
						$this->position=$offset;
						return true;
					}
					else {
						return false;
					}
					break;
				
				case SEEK_CUR:
					if (($this->position<=self::BUFFER_SIZE) && (($this->position+$offset)<=self::BUFFER_SIZE)) {
						$this->position+=$offset;
					}
					else {
						return false;
					}
					break;
				
				case SEEK_END:
					//is not possible. filesize is not known
					return false;
					break;
				
				default:
					return false;
			}
			
			
		}
    }

    /**
     * return true if the stream pointer is over or at the end of stream
     *
     * @return bool
     */
    public function stream_eof() {
		if ($this->is_seekable()) {
			return feof($this->resource);
		}
		else {
			if ($this->position<self::BUFFER_SIZE) {
				return false;
			}
		}
	}

    /**
     * force writing stream contents to disk
     *
     * @return bool
     */
    public function stream_flush() {
		return fflush($this->resource);
	}

    /**
     * locks stream
     *
     * @param mode $operation
     * @return bool
     */
    public function stream_lock($operation) {
		return flock($this->resource,$operation);
	}

    /**
     * Set sream options
     *
     * @param int $option
     * @param int $arg1
     * @param int $arg2
     * @return bool
     */
    public function stream_set_option($option , $arg1 , $arg2) {
		switch ($option) {
			case STREAM_OPTION_BLOCKING:
				return stream_set_blocking($this->resource,$arg1);
				break;
			
			case STREAM_OPTION_READ_TIMEOUT:
				return stream_set_timeout($this->resource,$arg1,$arg2);
				break;
			
			case STREAM_OPTION_WRITE_BUFFER:
				return stream_set_write_buffer($this->resource,$arg1);
				break;
			
			default:
				return false;
		}
	}

    /**
     * return stream stat
     *
     * @return array
     */
    public function stream_stat() {
		return fstat($this->resource);
	}

    /**
     * write to the stream
     *
     * @param string $data
     * @return int
     */
    public function stream_write($data) {
		if ($this->is_seekable()) {
			return fwrite($this->resource,$data);
		}
		else {
			$real_pos=ftell($this->resource);
			if ($real_pos==$this->position) {
				$result=fwrite($this->resource,$data);
				$this->position=ftell($this->resource);
				return $result;
			}
			//else: real and internal position is out of sync. no write possible at current position
		}
		
		return false;
	}
    
}

stream_wrapper_register("semiseekable", "SemiSeekableStream") or die("Failed to register SemiSeekableStream protocol");

